package com.bondar.hashmap;

import java.util.Iterator;
import java.util.NoSuchElementException;

import com.bondar.interfaces.MyMap;

public class MyHashMap<Key> implements MyMap<Key> {
	private int capacity;
	private double loadFactor;
	protected SimpleEntry<Key> table[];
	protected int size;

	/**
	 * Create new MyHashMap with fields capacity and loadFactor
	 * 
	 * @param capacity
	 * @param loadFactor
	 */
	public MyHashMap(int capacity, double loadFactor)
			throws IllegalArgumentException {
		if (capacity < 0 || loadFactor <= 0.0) {
			throw new IllegalArgumentException();
		}
		
		this.capacity = capacity;
		this.loadFactor = loadFactor;
		table = new SimpleEntry[capacity];
		size = 0;
	}

	/**
	 * Create new MyHashMap with fields capacity and loadFactor=0.75
	 * 
	 * @param capacity
	 */
	public MyHashMap(int capacity) throws IllegalArgumentException {
		this(capacity, 0.75);
	}

	/**
	 * Create new MyHashMap with fields capacity=16 and loadFactor=0.75
	 */
	public MyHashMap() {
		this(16, 0.75);
	}

	@Override
	public void clear() {
		table = new SimpleEntry[capacity];
		size = 0;
	}

	@Override
	public boolean containsKey(Key key) {
		return getEntry(key) != null;
	}

	@Override
	public boolean containsValue(Object value) {
		if (size <= 0) {
			return false;
		}
		
		for (int i = 0; i < table.length; i++) {
			for (SimpleEntry<Key> entry = table[i]; entry != null; entry = entry.next) {
				if (value != null && entry.getValue().equals(value)) {
					return true;
				}
				if (value == null && entry.getValue() == null) {
					return true;
				}
			}
		}

		return false;
	}
	
	private SimpleEntry<Key> getEntry(Key key) {
		if (size <= 0) {
			return null;
		}
		
		// get hash of key
		int hash = (key == null) ? 0 : key.hashCode();
		// find index
		int index = hash & (table.length - 1);
		// get SimpleEntry<Key>
		SimpleEntry<Key> entry = table[index];
		for (; entry != null; entry = entry.next) {
			if (key == entry.key || (key != null && entry.getKey().equals(key))) {
				return entry;
			}
		}

		return null;
	}

	@Override
	public Object get(Key key) {
		SimpleEntry<Key> entry = getEntry(key); 
		return entry == null ? null : entry.getValue();
	}

	@Override
	public boolean isEmpty() {
		return size == 0;
	}

	@Override
	public Object put(Key key, Object value) {
		if (size >= capacity * loadFactor) {
			resize(2 * capacity);
		}

		// make hash of key
		int hash = key == null ? 0 : key.hashCode();
		// find index to insert element
		int index = hash & (table.length - 1);
		// if there is element we return this old value and rewrite it
		for (SimpleEntry<Key> entry = table[index]; entry != null; entry = entry.next) {
			if (hash == entry.getKey().hashCode()
					&& (entry.equals(key) || key == entry.key)) {
				return entry.setValue(value);
			}
		}
		++size;
		return table[index] = new SimpleEntry<Key>(hash, key, value,
				table[index]);
	}

	private void resize(int capacity) {
		this.capacity = capacity;

		SimpleEntry<Key>[] tmp = new SimpleEntry[capacity];
		for (int i = 0; i < table.length; i++) {
			SimpleEntry<Key> newEntry;
			SimpleEntry<Key> entry = table[i];
			while (entry != null) {
				SimpleEntry<Key> next = entry.next;
				newEntry = entry;
				// recalculate new hashCode
				int hash = newEntry.getKey().hashCode();
				int index = hash & (tmp.length - 1);
				// add this element to the HEAD of list (entry)
				newEntry.next = tmp[index];
				tmp[index] = newEntry;
				entry = next;

			}
		}

		table = tmp;
	}

	@Override
	public Object remove(Key key) {
		if (size <= 0) {
			return null;
		}
		// get hash of key
		int hash = key == null ? 0 : key.hashCode();
		// find index
		int index = hash & (table.length - 1);

		// get SimpleEntry
		Object data = null;
		SimpleEntry<Key> entry = table[index];
		SimpleEntry<Key> prev = table[index];

		for (; entry != null; entry = entry.next) {

			if (key == entry.key || (key != null && entry.getKey().equals(key))) {
				--size;
				// is one element in the entry?
				if (entry == prev) {
					data = entry.getValue();
					table[index] = null;
				} else {
					prev.next = entry.next;
					data = entry.getValue();
					entry = null;
				}

				return data;
			}

			prev = entry;
		}

		return data;
	}

	@Override
	public int size() {
		return size;
	}

	@Override
	public Iterator<Entry<Key>> entryIterator() {
		return new Iterator<Entry<Key>>() {
			int index = 0;
			SimpleEntry<Key>[] se = table;
			int elements = size - 1;
			SimpleEntry<Key> next;
			boolean hasNext = true;

			@Override
			public boolean hasNext() {
				return hasNext;
			}

			@Override
			public SimpleEntry<Key> next() {
				if (elements < 0) {
					throw new NoSuchElementException();
				}
				if (elements == 0) {
					hasNext = false;
				}
				while (index < se.length && (next = se[index++]) == null);
				elements--;
				return next;
			}

			@Override
			public void remove() {
			}
		};
	}

	/**
	 * Simple Entry, alse use in HashSet in iterator
	 *
	 * @param <Key>
	 */
	public static class SimpleEntry<Key> implements
			MyMap.Entry<Key> {
		private Key key;
		private Object value;
		private int hashCode;
		private SimpleEntry<Key> next;

		public SimpleEntry(int hash, Key key, Object value,
				SimpleEntry<Key> next) {
			this.key = key;
			this.value = value;
			this.next = next;
			this.hashCode = hash;
		}

		@Override
		public boolean equals(Object obj) {
			if (obj == this) {
				return true;
			}
			if (!(obj instanceof SimpleEntry)) {
				return false;
			}

			SimpleEntry<Key> se = (SimpleEntry<Key>) obj;
			return (key == null ? se.key == null : key.equals(se.key)
					&& value == null ? se.value == null : value
					.equals(se.value));
		}

		@Override
		public int hashCode() {
			int result = hashCode;
			if (result == 0) {
				result = 17;
				result = 31 * result + (this.key == null ? 0 : this.key.hashCode());
				result = 31 * result + (this.value == null ? 0 : this.value.hashCode());
				hashCode = result;
			}

			return result;
		}

		@Override
		public Key getKey() {
			return key;
		}

		@Override
		public Object getValue() {
			return value;
		}

		@Override
		public Object setValue(Object value) {
			Object oldVal = this.value;
			this.value = value;
			return oldVal;
		}

	}

}
